﻿using System;
using Notoric.Commanding.Holders;

namespace Notoric.Commanding.CommandBase
{
    /// <summary>
    /// Extends the <see cref="CommandBase"/> class with support for single use commands (i.e. those that can be executed once and not undone).
    /// </summary>
    /// <remarks>
    /// The execution of the command is blocked after the first successful execution.
    /// </remarks>
    public abstract class SingleUseCommandBase : CommandBase
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="SingleUseCommandBase"/> instance.
        /// </summary>
        public SingleUseCommandBase()
        {
            Executed = false;
        }

        #endregion

        #region ICommand Members
        
        /// <summary>
        /// Determines whether the command can execute in its current state.
        /// </summary>
        /// <remarks>
        /// The default implementation of this method returns true if the <see cref="Notoric.Commanding.CommandBase.CommandBase.CanExecute"/> 
        /// method returns <see langword="true"/> and the command was not yet executed (i.e. <see cref="Executed"/> equals <see langword="false"/>.
        /// </remarks>
        /// <param name="parameter">
        /// Data used by the command. If the command does not require data to be passed, this object can be set to <see langword="null"/>.
        /// </param>
        /// <returns><see langword="true"/> if this command can be executed; <see langword="false"/> otherwise</returns>
        /// <seealso cref="Executed"/>
        public override bool CanExecute(object parameter)
        {
 	        return base.CanExecute(parameter) && !Executed;
        }

        /// <summary>
        /// Executes the command.
        /// </summary>
        /// <remarks>
        /// If some parameters are bound to subcommands, the subcommands that were not yet executed are invoked before the <see cref="CommandOperation"/>
        /// is called.
        /// </remarks>
        /// <param name="parameter">
        /// Data used by the command. If the command does not require data to be passed, this object can be set to <see langword="null"/>.
        /// </param>
        /// <exception cref="ExecutionException">
        /// If the command cannot execute in its current state.
        /// </exception>
        /// <seealso cref="CanExecute"/>
        /// <seealso cref="CommandOperation"/>
        public override void Execute(object parameter)
        {
            if (!CanExecute(parameter))
                throw new ExecutionException("The command cannot execute, CanExecute returns false!");

            foreach (Parameter param in parameterSet)
            {
                if (param.ValueHolder.ValueState == ValueState.Unknown)
                {
                    if (param.Command != null && param.Command.CanExecute(parameter))
                        param.Command.Execute(parameter);
                }
            }

            try
            {
                CommandOperation(parameter);
            }
            catch (Exception e)
            {
                throw new Exception("An exception prevented the command from executing correctly!", e);
            }

            LastExecuted = DateTime.Now;
            Executed = true;

            RaiseCanExecuteChanged();
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the value indicating whether the command was already executed or not.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.Boolean"/>
        /// </para>
        /// <para>
        /// Indicates whether the command was already executed (true) or not (false). 
        /// If the command was already executed, it blocks and cannot be executed again.
        /// </para>
        /// </value>
        public bool Executed
        {
            get;
            private set;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Binds the value of a given parameter to the value of a given parameter of a given idempotent command.
        /// </summary>
        /// <remarks>
        /// <para>
        /// This method allows an easy creation of macro commands. Binding the parameter values to subcommands results in creating
        /// an execution tree where the leaf commands (those whose values are not bound to other commands) are executed first.
        /// The order of execution is not defined for the commands at the same level of the tree.
        /// </para>
        /// <example>
        /// RootCommand(ParamA: 5, ParamB: Bound to SubCommand1.ParamB, ParamC: Bound to SubCommand2.ParamC)<br/>
        /// SubCommand1(Param A: Bound to SubCommand3.ParamA, ParamB: Bound to SubCommand3.ParamB)<br/>
        /// SubCommand2(ParamC: Result of the command execution)<br/>
        /// SubCommand3(ParamA, ParamB: Results of the command execution)<br/>
        /// <br/>
        /// Results in the follwoing order of execution:<br/>
        /// 1. SubCommand3<br/>
        /// 2. SubCommand1 and SubCommand2 in any order<br/>
        /// 3. RootCommand
        /// </example>
        /// <para>
        /// Idempotent command can be bound only to other idempotent commands.
        /// </para>
        /// </remarks>
        /// <param name="name">Name of the parameter whose value is being bound to a subcommand</param>
        /// <param name="command">Instance of the <see cref="SingleUseCommandBase"/> class to which the value is being bound</param>
        /// <param name="outputName">Name of the parameter of the <paramref name="command"/> to wich the value is being bound</param>
        /// <exception cref="ParameterDoesNotExistException">
        /// The parameter being bound does not exist on this command or the target parameter does not exist on the other command.
        /// </exception>
        /// <exception cref="IncompatibleTypesException">
        /// The parameters being bound do not have compatible value types.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// Some of the parameters is null.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// The parameter beind bound (i.e. the one identified by <paramref name="name"/>) is output.
        /// </exception>
        public void BindParameterToCommand(string name, SingleUseCommandBase command, string outputName)
        {
            BindParameterToCommandInternal(name, command, outputName);
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Implements the logic of the command.
        /// </summary>
        /// <param name="parameter">Parameter for the execution.</param>
        protected abstract void CommandOperation(object parameter);

        #endregion
    }
}
